package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
)

//基于map类型实现函数式客户关系管理系统(客户增删改查)。客户信息包括姓名、年龄、职业 切片嵌套map 可以保证数据的有序性
//将客户信息保存到一个文件中
//00ies:[games] name:wxd worker:0ops]]
/*
	filedatapath  这是数据保存的地址
*/
var filedatapath = "D:\\go\\src\\code.oldboyedu.com\\studygo\\day03\\oldbodyday03_homework\\person.txt"

/*
	Exists()
	该段代码的功能是通过传入进来的形参判断保存数据的文件或目录是否存在.
	如果通过os.Stat()函数获取到了文件信息，则返回true 文件存在
	如果未获取到文件信息，则通过os.IsExist()函数判断err是否表示指定的文件或目录已存在，如果已存在，返回true 反之返回false
*/
func Exists(path string) bool {
	_, err := os.Stat(path) //os.Stat获取文件信息,如果文件存在 返回true  反之，根据错误提示信息进一步判断
	if err == nil {
		fmt.Println("File exist")
		return true
	}
	//go1.15之前是使用IsExist函数判断，go1.15版本之后使用IsNotExist函数判断
	if os.IsNotExist(err) {
		fmt.Println("File not exist")
		return false
	}
	fmt.Println("File error")
	return false
}

/*
	IoutilWriteFile() ioutil.WriteFile方法写文件
		函数传入形参与返回值
			func WriteFile(filename string, data []byte, perm fs.FileMode) error {
				return os.WriteFile(filename, data, perm)
			}
		优点
			1、该方法写文件时不需要判断文件是否存在,如果文件不存在 会自动创建，反之，则会覆盖原来的文件内容
			2、支持多种数据形式的写入，可以写入二进制文件、文本文件等
			3、它提供写文件失败时返回错误信息
		缺点
			1、将整个文件内容读取到内存中，进行修改和写入操作，如果文件过大，会导致内存占用过高
			2、使用的是标准库中的锁机制，因此在高并发场景下，可能存在文件并发写入效率低下的问题。
*/
func IoutilWriteFile(fileName, data string) {
	if err := ioutil.WriteFile(fileName, []byte(data), 0777); err != nil {
		fmt.Println("write file failed", err)
	} else {
		fmt.Println("write file success")
	}
}

/*
	IoutilReadFile() ioutil.ReadFile()方法读文件
		函数传入的形参与返回值
			func ReadFile(filename string) ([]byte, error) {
				return os.ReadFile(filename)
			}
		优点
			1、读取文件只需要传入一个文件名作为形参，会将文件的内容作为一个字节数组返回，可以进行反序列化操作
			2、不需要使用os.Open()函数与defer Close()函数打开与关闭文件 这个操作系统自动帮我们完成
		缺点
			1、只适合读取小文件，不适合读取大文件
*/
func IoutilReadFile(fileName string) []map[string]interface{} {
	var chunk []byte
	var personData []map[string]interface{}
	if Exists(fileName) {
		data, err := ioutil.ReadFile(fileName)
		if err != nil {
			fmt.Println("读取文件失败", err)
		}
		chunk = append(chunk, data...) //将读取到的文件保存到chunk中
	}
	err := json.Unmarshal(chunk, &personData) //将[]byte类型的数据反序列为[]map[string]interface{}，使得读文件时按原来的输入格式读出来
	if err != nil {
		fmt.Println("反序列化失败", err)
	}
	return personData
}

/*
	getInput() (string, string, []string)
	该函数用于输入信息时使用,它有三个返回值,分别是string, string, []string
	1、怎么输入值？
		通过Scan()函数来接收屏幕输入
	2、对于一个切片类型，其中每个元素都是一个键为字符串类型、值为空接口类型的 map
		因为第三个参数是一个字符串类型的切片 因此可能有一个或多个值 并且在输入时需要判断输入是否结束且要把切片的元素追加到[]hobbies中
	3、为什么需要返回值？
		因为在增加客户信息的addCustomersmsg()函数中需要接收该处的值
*/
func getInput() (string, string, []string) {
	var (
		worker  string
		name    string
		hobbies []string
	)
	fmt.Println("输入职位")
	fmt.Scan(&worker)
	fmt.Println("输入姓名")
	fmt.Scan(&name)
	for {
		var hobby string
		fmt.Print("爱好（输入空字符串结束）：")
		fmt.Scan(&hobby)
		if hobby == "exit" {
			break
		}
		hobbies = append(hobbies, hobby)
	}
	return worker, name, hobbies
}

//增加客户信息
func addCustomersmsg(worker, name string, hobby []string) map[string]interface{} {
	curstomersmap := make(map[string]interface{})
	curstomersmap["worker"] = worker
	curstomersmap["name"] = name
	curstomersmap["hobbies"] = hobby
	return curstomersmap
}

/*
	1、该函数主要用于通过传递的实参来获取对应的客户信息
	2、形参id是想要查找的用户id 且对传入进来的id形参进行了判断，用一个bool类型来表明id是否判断成功 成功为true
	3、形参msg的主要作用是用来判断输入的id是否在整个数据切片中
		如果不存在，则需要返回id判断失败的bool类型结果和一个nil类型的map
		如果存在 则返回id判断成功的bool类型和一个id所对应的map信息 msg[id]
		实际上 此处的id 相当于切片的下标  msg[id] 实际上最后打印的是切片中id对应的value
*/
func viewOncemsg(id int, msg []map[string]interface{}) (bool, map[string]interface{}) {
	if id < 0 || id >= len(msg) { //如果传递进来的id形参值小于0或者大于等于数据切片的长度 则id不存在
		return false, nil
	}
	return true, msg[id]
}

/*
	方法一、打印全部用户信息
func viewAllmsg(slicemap []map[string]interface{}) []map[string]interface{} {
	return slicemap //此处定义的形参<===>返回值  在函数的调用处又重新定义了一个allmsg ，用于接收返回值
	//  索引      0                              1
	//	值  map[string]interface{}     map[string]interface{}
	//  string对应的就是     worker、name、hobbies
	// interface{}对应的就是  worker、name、hobbies这些变量的类型
}
*/
//打印全部的用户信息 通过读持久化文件，将数据反序列后打印出来
func viewAllmsg(filename string) []map[string]interface{} {
	return IoutilReadFile(filename)
}

//通过id查到对应用户数据并删除。最后将新的数据通过序列化操作写入到持久化文件中
func delmsg(id int, slicemap []map[string]interface{}) (bool, []map[string]interface{}) {
	if id < 0 && id >= len(slicemap) {
		return false, nil
	}
	slicemap = append(slicemap[:id], slicemap[id+1:]...)
	return true, slicemap
}

//通过id查到对应用户数据并修改。最后将新的数据通过序列化操作写入到持久化文件中
func updatemsg(id int, slicemap []map[string]interface{}) (bool, []map[string]interface{}) {
	if id < 0 || id >= len(slicemap) {
		return false, slicemap
	}
	worker, name, hobbies := getInput()
	slicemap[id]["worker"] = worker
	slicemap[id]["name"] = name
	slicemap[id]["hobbies"] = hobbies
	return true, slicemap
}
func main() {
	/*
		1、在死循环外首先定义一个元素为map类型的切片，用于存储客户信息 并初始化内存空间
		2、编写一个死循环函数，作用: 用来循环打印功能信息、输入不同的序号执行不同的函数
	*/
	personSliceMap := make([]map[string]interface{}, 0)
	//执行该函数后，先是会去持久化文件中读数据
	personSliceMap = IoutilReadFile(filedatapath)
	for {
		fmt.Println(`
			0.增加客户信息
			1.查看单个客户信息
			2.查看所有客户信息
			3.删除客户信息
			4.修改客户信息
			5.退出系统
		`)
		var choice int
		fmt.Println("输入你的选择:")
		fmt.Scan(&choice)
		switch choice {
		case 0:
			/*
				方法一、将输入到内存中的数据保存到内存中
					fmt.Println("增加客户信息")
					worker, name, hobby := getInput()
					curstomersmap := addCustomersmsg(worker, name, hobby)
					personSliceMap = append(personSliceMap, curstomersmap)
					fmt.Println("增加客户信息成功")
			*/
			/*
				方法二、将增加的数据通过序列化后，在调用写函数持久化到数据文件中
			*/
			fmt.Println("增加客户信息")
			worker, name, hobby := getInput()
			curstomersmap := addCustomersmsg(worker, name, hobby)
			personSliceMap = append(personSliceMap, curstomersmap)
			//将保存到[]map[string]interface{}中的数据进行序列化为byte类型数组
			jsonStr, _ := json.Marshal(personSliceMap)
			//在调用给数据文件写数据的函数，将序列化后的数组数据转换为string类型后输入到文件中
			IoutilWriteFile(filedatapath, string(jsonStr))
		case 1:
			fmt.Println("查看单个客户信息-->输入客户的id")
			var id int //定义一个int类型的id 承担客户id 进行传参
			fmt.Scan(&id)
			//首先传入实参id和存数据的切片，且该函数有两个返回值success、kjk
			//其次 success这个返回值是一个bool类型，主要是用来判断输入的id是否在数据切片中存在；如果存在 打印id对应用户的信息，反之则报不存在
			//最后 kjk是一个键为string类型、值为interface{}类型的map 主要是接收id对应的存在用户的信息 实际就是获取map类型中key对应的value
			success, kjk := viewOncemsg(id, personSliceMap)
			if success {
				/*
					方法二、 从持久化的数据文件中反序列化后打印某个客户信息
						当判断id在切片类型的map中时 调用读函数文件，进行反序列化文件中的数据 并将id对应的数据进行反序列化后打印出来
				*/
				personSliceMap = IoutilReadFile(filedatapath)
				fmt.Println(personSliceMap[id])
				/*
					方法一、从保存在内存中的数据直接打印对应的信息
				*/
				fmt.Printf("客户ID：%d,职位:%s,姓名:%s,爱好:%v\n", id, kjk["worker"], kjk["name"], kjk["hobbies"])
			} else {
				fmt.Println("客户不存在~~~")
			}
		case 2:
			fmt.Println("查看所有客户信息")
			/*
				方法一、从保存在内存中的数据读出来
				allmsg := viewAllmsg(personSliceMap)
				//因此allmsg是一个元素为map类型的切片，因此使用切片的循环方法 在打印时因元素是map类型，
				//因此查key对应的值用的就是map的查找方法 valuemap["worker"]
				for index, valuemap := range allmsg {
					fmt.Printf("客户ID%d,职位:%s,姓名:%s,爱好:%v\n", index, valuemap["worker"], valuemap["name"], valuemap["hobbies"])
				}
			*/
			/*
				方法二、 从持久化到数据文件中读
					调用viewAllmsg函数，再在函数中调用读函数  将保存在持久化数据文件中的数据反序列化后打印出来
			*/
			personSliceMap = viewAllmsg(filedatapath)
			fmt.Println("反序列化后的数据:", personSliceMap)
		case 3:
			fmt.Println("删除客户信息。输入删除客户的id")
			var id int
			var success bool
			fmt.Scan(&id)
			success, personSliceMap = delmsg(id, personSliceMap)
			if success {
				/*
					方法二、 从持久化的数据文件中反序列化读到对应id的客户信息
							如果读到对应的id，则调用删除信息函数进行删除，返回一个[]map[string]interface{}类型
							再将该类型的数据进行序列化后调用写函数重新写入到持久化文件中
				*/
				jsonStr, _ := json.Marshal(personSliceMap)
				IoutilWriteFile(filedatapath, string(jsonStr))
				fmt.Println("客户信息删除成功!!!")
			} else {
				fmt.Println("客户信息删除失败~~~")
			}
		case 4:
			fmt.Println("修改客户信息.输入修改客户的id")
			var id int
			fmt.Scan(&id)
			success, personSliceMap := updatemsg(id, personSliceMap)
			if success {
				/*
					调用更新函数将数据更新后，返回[]map[string]interface{}类型，然后在通过序列化数据，调用写函数，将更新后的数据重新写入
						到持久化文件中
				*/
				jsonStr, _ := json.Marshal(personSliceMap)
				IoutilWriteFile(filedatapath, string(jsonStr))
				fmt.Println("客户信息修改成功!!!", personSliceMap[id])
			} else {
				fmt.Println("客户信息修改失败~~~")
			}
		case 5:
			os.Exit(10002) //退出系统
		default:
			fmt.Println("输入有误~~~")
			os.Exit(10001)
		}
	}
}
